/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package nsip.components;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import nsip.NsipAgent;
import nsip.components.ds.Catalog;
import nsip.components.ds.Inventory;
import nsip.components.ds.PriceTrackerDataBase;
import nsip.transport.NsipTransport;
import nsip.components.ds.InventoryDataBase;
import nsip.components.ds.OrderDataBase;
import nsip.components.ds.SimpleInventoryMap;
import nsip.components.ds.StatisticType;
import nsip.components.ds.TimeAwareInventory;
import nsip.components.ds.TimeAwareInventory.Interval;
import nsip.components.ds.bi.NormalizedHistory;
import nsip.graph.ConstantInfo;
import nsip.graph.GraphInfo;
import nsip.graph.GraphWindow;
import nsip.graph.HistorybasedGraphRenderer;
import nsip.model.RFQSet;
import nsip.model.messages.CustomerOrderBundle;
import nsip.model.messages.SupplierOfferBundle;
import nsip.model.DataRecord;
import nsip.model.Item;
import nsip.model.ItemStatus;
import nsip.model.ItemType;
import nsip.model.messages.EndOfDay;
import nsip.model.messages.NewDay;
import nsip.model.messages.SimulationAboutToStart;
import nsip.util.LazyInitializingMap;
import nsip.util.Const;
import nsip.util.History;
import nsip.util.Utils;
import se.sics.tasim.props.DeliveryNotice;
import se.sics.tasim.props.OrderBundle;
import se.sics.tasim.props.RFQBundle;
import se.sics.tasim.props.StartInfo;

/**
 *
 * @author nico.rehwaldt
 */
public class SupplyAgent extends NsipAgent {

    private static final Logger logger = Logger.getLogger("SupplyChainManagementAgent");

    private Random random = new Random();

    private final Inventory orderedItems = new Inventory();

    private Map<Integer, History<DemandDetail>> productDemandHistoryMap;
    private GraphWindow productDemandGraphWindow;

    private List<History<Integer>> productPriceHistory;
    
    private GraphWindow productPriceGraphWindow;
    private final PriceTrackerDataBase priceTracker;

    private Map<Integer, History<Integer>> productBaseDemandMap;
    
    private int lastDayToPreorderProducts;

    public SupplyAgent(InformationSystem is,
                       NsipTransport transport) {
                       
         super(is, transport);

         statistics().addMetric(StatisticType.AVERAGE_SUPPLY_DISCOUNT_RATIO, new NormalizedHistory(0.3, 0.8));
         this.priceTracker = is().getPriceTracker();
    }

    public void init(SimulationAboutToStart info) {
        
        int[] goods = new int[] { 100, 101, 110, 111, 200, 210, 300, 301, 400, 401 };
        HistorybasedGraphRenderer[] demandRenderers = new HistorybasedGraphRenderer[goods.length];
        
        productDemandHistoryMap = new HashMap();
        productBaseDemandMap = new HashMap();
        
        for (int i = 0; i < goods.length; i++) {
            int goodID = goods[i];
            
            History<DemandDetail> history = new History<DemandDetail>();
            History<Integer> baseDemandHistory = new BaseDemandHistory(history);
            
            productDemandHistoryMap.put(goodID, history);
            productBaseDemandMap.put(goodID, baseDemandHistory);
            
            demandRenderers[i] = new InventoryDemandGraphRenderer(history, goodID);
        }

        productDemandGraphWindow = new GraphWindow("Product Demands", demandRenderers);
        productDemandGraphWindow.setVisible(true);

        productPriceHistory = new ArrayList(20);

        HistorybasedGraphRenderer[] priceRenderers = new HistorybasedGraphRenderer[8];
        for (int i = 1; i < 9; i++) {
            History history = new History(0);
            productPriceHistory.add(history);
            priceRenderers[i - 1] = new ProductPriceGraphRenderer(
                history, 200, i, is().getCatalog().getPartInfo(200).getBasePrice());
        }

        productPriceGraphWindow = new GraphWindow("Prices for product 200", priceRenderers);
        productPriceGraphWindow.setVisible(true);
    }

    /**
     * Allows the implementation of an agent to reject rfqs which are
     * totally unacceptable for him and thus to reduce the "problem space".
     *
     * @param rfq
     *
     * @return true if the agent accepts the specified rfq, false otherwise
     */
    public boolean acceptsRFQ(Item rfq) {
        int partsPrice = rfq.metadata().getInfo().getPartsPrice();
        if (partsPrice > rfq.getPricePerUnit()) {
            return false;
        }

        // Check if ...
        int currentDate = is().getCurrentDate();
        int timeToProduce = 6; // currentDate > 25 ? 5 : 6;
        if (rfq.getDueDate() < currentDate + timeToProduce) {
            return false;
        }
        
        return true;
    }

    /**
     * Evaluate the specified rfq set and returns a value between 0
     * (not acceptable) and 1 (totally great)
     *
     * @param rfqSet
     * @return value of rfq set
     */
    public double evaluateRFQSet(RFQSet rfqSet) {
        return 0.7;
    }

    private void handleSupplierOffers(SupplierOfferBundle bundle) {

        OrderBundleMap orderMap = new OrderBundleMap();
        
        InventoryDataBase inventory = is().getInventory();
        OrderDataBase supplierDB = is().getSupplierDB();
        
        // Earliest complete is always after partial offers so the offer
        // bundle is traversed backwards to always accept earliest offer
        // instead of the partial (the server will ignore the second
        // order for the same offer).

        List<Item> offers = bundle.getOffers();

        double supplierDiscountRatioSum = 0.0;

        // Accept full offers instead of partial ones
        Collections.reverse(offers);
        
        for (Item offer: offers) {
            Item rfq = offer.getParent();

            supplierDiscountRatioSum += (0.0 + offer.getPricePerUnit()) /
                    is().getCatalog().getPartInfo(offer.getProductID()).getBasePrice();
            
            // We might also get partial offers here and those appear at the
            // end of the list. If we have a partial offer, we will see it cause
            // the rfq corresponding to this offer was already processed when
            // checking the "normal" offer.
            if (rfq.hasStatus(ItemStatus.PROCESSED)) {
                logger.warning("[Skipping] " + offer + ", " + rfq + " already processed");
                continue;
            }

            supplierDB.add(offer);
            
            // Check how many supplier offers differ from the rfqs we have
            // sent and log that (maybe interesting later).
            int quantityDiffToRFQ = rfq.getQuantity() - offer.getQuantity();
            
            if (quantityDiffToRFQ != 0) {
                logger.warning(
                        offer + " ships with " +
                        offer.getQuantity() + " / " + rfq.getQuantity() +
                        " items");
            }

            if (rfq.getDueDate() != offer.getDueDate()) {
                logger.warning(
                        offer + " will ship day " +
                        offer.getDueDate() + " / " +
                        rfq.getDueDate());
            }
            
            priceTracker.addPrice(
                    offer.getProductID(),
                    offer.getDueDate(),
                    offer.getPricePerUnit());
                    
            rfq.setStatus(ItemStatus.PROCESSED);
            offer.setStatus(ItemStatus.PROCESSED);

            // TODO: Make maximimum due date variable
            if (offer.getDueDate() > 220 - 3) {
                logger.warning("Skipping " + offer + " will arrive to late");
                continue;
            }

            // Quantity == 0 is just a price note
            // We won't create an order for that and won't do anything else
            if (offer.getQuantity() == 0) {
                logger.info("[PriceNote] " + offer);
                continue;
            }
            
            Item order = offer.copy().update(new DataRecord.SupplierOrder());

            // Save in database
            supplierDB.add(order);
            
            orderMap.get(offer.getPeerAddress())
                    .addOrder(order.getId(), offer.getId());
            
            // Assume that the suppliers will be able to deliver the
            // components NOW and remove this demand.
            inventory.demanded().remove(rfq.getProductID(), offer.getQuantity(), offer.getDueDate());

            // Add the ordered amount to our inventory
            orderedItems.add(rfq.getProductID(), offer.getQuantity());
        }

        statistics()
            .getMetric(StatisticType.AVERAGE_SUPPLY_DISCOUNT_RATIO)
            .update(supplierDiscountRatioSum / offers.size());
        
        transport().sendMessages(orderMap);
    }

    private void handleCustomerOrders(CustomerOrderBundle bundle) {
        
        TimeAwareInventory demand = is().getInventory().demanded();
        SimpleInventoryMap<Integer> ordersByLengthMap = new SimpleInventoryMap();
        
        // Add the component demand for the new customer orders
        for (Item order: bundle.getOrders()) {
            int productID = order.getProductID();
            int quantity = order.getQuantity();
            Integer[] components = is().getCatalog().getInfo(productID).getComponentIDs();
            
            // Give each order two days to produce it
            // TODO: Optimize according to what is betta.
            demand.add(components, quantity, order.getDueDate() - Const.DAYS_TO_PRODUCE);
            
            ordersByLengthMap.add(order.getDueDate() - is().getCurrentDate(), 1);
        }

        logger.info("[Incomming Customer Orders] " + ordersByLengthMap);
    }

    private void handleSupplierDelivery(DeliveryNotice notice) {
        InventoryDataBase inventory = is().getInventory();
        
        // In TAC SCM only suppliers delivers to manufacturers
        // Mark the delivered orders as delivered.
        for (int i = 0, n = notice.size(); i < n; i++) {
            Item order = is().getSupplierDB().get(ItemType.ORDER, notice.getOrderID(i));
            
            if (order != null) {
                // Set order delivered
                order.setStatus(ItemStatus.PROCESSED);

                if (notice.getQuantity(i) != order.getQuantity()) {
                    logger.warning("[Supplier delivery] " + notice.getQuantity(i) + "/" + order.getQuantity());
                }
                
                // Update inventory for tomorrow by adding goods
                inventory.tomorrow().add(
                    notice.getProductID(i),
                    notice.getQuantity(i));

                orderedItems.add(
                        notice.getProductID(i),
                        -notice.getQuantity(i));
                
                statistics().counters().increment(StatisticType.SUPPLIER_DELIVERY);
            } else {
                logger.log(Level.WARNING,
                    "received delivery of unknown order {0}",
                    notice.getOrderID(i));
            }
        }
    }

    private static final int OPTIMAL_GOODS_PER_ORDER = 40;
    
    private void doDailyPostProcessing() {

        TimeAwareInventory demand = is().getInventory().demanded();
        Inventory inventory = is().getInventory().tomorrow();

        RFQBundleMap rfqStoreMap = new RFQBundleMap(is().getSupplierDB());
        
        // Order the components needed to fulfill the new orders from the
        // suppliers.
        Catalog catalog = is().getCatalog();

        Map<Integer, DemandDetail> demandMap = new LazyInitializingMap<Integer, DemandDetail>() {
            @Override
            protected DemandDetail initialize(Object key) {
                return new DemandDetail();
            }
        };

        int currentDate = is().getCurrentDate();
        for (Interval interval: Interval.values()) {
            Map<Integer, Integer> intervalMap = demand.intervalMap(interval);

            for (Map.Entry<Integer, Integer> entry: intervalMap.entrySet()) {
                int partID = entry.getKey();
                demandMap.get(partID).set(interval, entry.getValue());
                
                // Always order a few products more (aproximate in the future)
                int demandedQuantity = (int) (entry.getValue() * (1.0 + interval.getReserve()));
                
                if (demandedQuantity <= 0) {
                    continue;
                }

                // Cut down demanded quantity to make sure that not thousands of
                // elements are ordered at the same time
                if (demandedQuantity > 300) {
                    demandedQuantity /= 2;
                }

                int quantityStillToOrder = demandedQuantity;

                // Dont do anything if we already have more that 300 items of this
                // type in the inventory
                if (inventory.get(partID) > 600 &&
                   (interval == Interval.IMMEDIATLY || interval == Interval.SHORT)) {
                    logger.warning("Ignoring requests to order product #" + partID);
                    continue;
                }

                List<String> suppliers = Utils.asList(catalog.getSuppliers(partID));

                if (suppliers.isEmpty()) {
                    // There should always be suppliers for all components so
                    // this point should never be reached.
                    logger.severe("No suppliers for product " + partID);
                    continue;
                }

                while (quantityStillToOrder > 0) {
                    // Pick random supplier and remove him
                    String supplier = suppliers.get(random.nextInt(suppliers.size()));
                    int orderQuantity = quantityStillToOrder;

                    if (orderQuantity > OPTIMAL_GOODS_PER_ORDER) {
                        orderQuantity = (int) (OPTIMAL_GOODS_PER_ORDER * 0.75) +
                                random.nextInt((orderQuantity - OPTIMAL_GOODS_PER_ORDER) / 2 + 1);
                    }

                    quantityStillToOrder -= orderQuantity;

                    // TODO: Specify reserve price per unit and penalty
                    Item rfq = new Item(new DataRecord.SupplierRFQ(
                            supplier, partID, orderQuantity,
                            Math.max(2, interval.getDueDate()) + currentDate));

                    rfqStoreMap.add(rfq);
                }
                
                // Don't remove demand from demand inventory, instead wait for next
                // day and see if the suppliers send us an offer for our RFQs.
            }
        }
        
        if (is().getCurrentDate() <= lastDayToPreorderProducts) {
            for (Map.Entry<String, Integer> entry: rfqStoreMap.getOrderCountMap().entrySet()) {
                String supplier = entry.getKey();
                
                int issuedRFQs = entry.getValue();
                if (issuedRFQs < 10) {
                    Integer[] partIDs = is().getCatalog().getPartsSuppliedBy(supplier);
                    int partID  = partIDs[random.nextInt(partIDs.length)];
                    int dayInAdvance = random.nextInt(8) + 10;
                    int baseDemand = productBaseDemandMap.get(partID).last();

                    if (baseDemand > 0) {
                        
                        if (baseDemand > 50) {
                            baseDemand = Math.max(baseDemand / 2, 40);
                        } else 
                        if (baseDemand > 30) {
                            baseDemand = (int) Math.max(baseDemand / 1.5, 25);
                        }

                        logger.fine("[PreOrdering] Ordering #" + partID + ": " + baseDemand + " in " + dayInAdvance);
                        
                        Item rfq = new Item(new DataRecord.SupplierRFQ(
                                supplier, partID, baseDemand,
                                is().getCurrentDate() + dayInAdvance));

                        rfqStoreMap.add(rfq);
                    }
                }
            }
        }

        logger.info("[Issued Supplier RFQs] " + rfqStoreMap.getOrderCountMap());

        // Update history
        for (Map.Entry<Integer, DemandDetail> entry: demandMap.entrySet()) {
            productDemandHistoryMap.get(entry.getKey()).add(entry.getValue());
        }
        
        statistics()
            .getMetric(StatisticType.ISSUED_SUPPLIER_RFQS_PER_ROUND)
            .update(rfqStoreMap.getIssuedOrders());
        
        transport().sendMessages(rfqStoreMap);
    }

    @Override
    public void receive(Object message) {
        if (message instanceof DeliveryNotice) {
            handleSupplierDelivery((DeliveryNotice) message);
        } else

        if (message instanceof CustomerOrderBundle) {
            handleCustomerOrders((CustomerOrderBundle) message);
        } else

        if (message instanceof SupplierOfferBundle) {
            handleSupplierOffers((SupplierOfferBundle) message);
        } else

        if (message instanceof EndOfDay) {
            doDailyPostProcessing();
        } else
        
        if (message instanceof NewDay) {
            for (int i = 1; i < 9; i++) {
                productPriceHistory.get(i - 1).add(
                    priceTracker.getPrice(200, is().getCurrentDate() + i));
            }
            
            productDemandGraphWindow.redraw();
            productPriceGraphWindow.redraw();
        } else

        if (message instanceof SimulationAboutToStart) {
            init((SimulationAboutToStart) message);
        } else

        if (message instanceof StartInfo) {
            lastDayToPreorderProducts = ((StartInfo) message).getNumberOfDays() - 20;
        }
    }
    
    @Override
    public Collection<Class> getMessageInterests() {
        return Arrays.asList(new Class[] {
            // ComponentCatalog.class,
            // BOMBundle.class,
            // StartInfo.class,
            // ActiveOrders.class,
            // BankStatus.class,
            // MarketReport.class,
            // CustomerRFQBundle.class,
            DeliveryNotice.class,
            // FactoryStatus.class,
            SupplierOfferBundle.class,
            CustomerOrderBundle.class,
            // AcceptedCustomerRFQsBundle.class,
            // PriceReport.class,
            EndOfDay.class,
            NewDay.class,
            SimulationAboutToStart.class,
            StartInfo.class
        });
    }

    private static class OrderBundleMap extends LazyInitializingMap<String, OrderBundle> {
        
        @Override
        public OrderBundle initialize(Object key) {
            return new OrderBundle();
        }
    }

    private static class RFQBundleMap extends LazyInitializingMap<String, RFQBundle> {

        private int count = 0;
        private OrderDataBase supplierDB;
        private SimpleInventoryMap<String> orderCountMap = new SimpleInventoryMap();

        public RFQBundleMap(OrderDataBase supplierDB) {
            this.supplierDB = supplierDB;
        }

        /**
         * Add an rfq to the rfqs contained in this bundle map
         * @param rfq
         */
        public void add(Item rfq) {
            String supplier = rfq.getPeerAddress();
            
            supplierDB.add(rfq);
            orderCountMap.add(supplier, 1);
            
            get(supplier).addRFQ(
                    rfq.getId(),
                    rfq.getProductID(),
                    rfq.getQuantity(),
                    rfq.getPricePerUnit(),
                    rfq.getDueDate(),
                    rfq.getPenalty());

            count++;
        }
        
        @Override
        protected RFQBundle initialize(Object key) {
            return new RFQBundle();
        }

        /**
         * Return map which contains the number of issued orders for each
         * supplier.
         * 
         * @return
         */
        public Map<String, Integer> getOrderCountMap() {
            return orderCountMap;
        }

        /**
         * Return the number of
         * @return
         */
        public int getIssuedOrders() {
            return count;
        }
    }

    /**
     * Class which stores the demand detail of a product on a particular day.
     */
    private static class DemandDetail {
        private final int[] values;

        public DemandDetail() {
            values = new int[Interval.values().length];
            Arrays.fill(values, 0);
        }
        
        public int get(Interval interval) {
            return values[interval.ordinal()];
        }

        public void set(Interval interval, int demand) {
            values[interval.ordinal()] = demand;
        }
    }

    private static class InventoryDemandGraphRenderer extends HistorybasedGraphRenderer<DemandDetail> {

        private static GraphInfo[] graphs = new GraphInfo[] {
            new GraphInfo("total", Color.RED),
            new GraphInfo("average", Color.ORANGE),
            new GraphInfo("3 day average", Color.BLUE)
        };

        public InventoryDemandGraphRenderer(History<DemandDetail> history, int good) {
            super(history, 230, "" + good);
        }

        @Override
        protected GraphInfo[] getGraphInfos() {
            return graphs;
        }

        @Override
        protected int[] extractData(DemandDetail item, History<DemandDetail> history) {

            List<DemandDetail> last = history.last(10);
                        
            int sum = 0;
            int count = 0;

            for (Interval i: Interval.values()) {
                sum += item.get(i);
                count++;
            }

            int todaysTotal = sum;
            int todaysAverage = sum / count;
            
            for (DemandDetail detail: last) {
                for (Interval i: Interval.values()) {
                    sum += detail.get(i);
                    count++;
                }
            }

            int normalizedAverage = sum / count;
            
            return new int[] {
                todaysTotal,
                todaysAverage,
                normalizedAverage
            };
        }
    }

    private static class ProductPriceGraphRenderer extends HistorybasedGraphRenderer<Integer> {

        private static GraphInfo[] graphs = new GraphInfo[] {
            new GraphInfo("today", Color.RED),
            new GraphInfo("normalized", Color.BLUE)
        };
        
        private final ConstantInfo[] constants;

        public ProductPriceGraphRenderer(History<Integer> history, int good, int day, int basePrice) {
            super(history, 230, good + "/" + day);

            this.constants = new ConstantInfo[] {
                new ConstantInfo(basePrice, Color.YELLOW)
            };
        }

        @Override
        protected GraphInfo[] getGraphInfos() {
            return graphs;
        }

        @Override
        public ConstantInfo[] getConstants() {
            return constants;
        }

        @Override
        protected int[] extractData(Integer item, History<Integer> history) {

            List<Integer> last = history.last(10);

            int todays = item;
            
            int sum = item;
            int count = 1;

            for (Integer price: last) {
                if (price == 0) continue;
                
                sum += price;
                count++;
            }

            int normalizedAverage = sum != 0 ? sum / count : 0;

            return new int[] {
                todays,
                normalizedAverage
            };
        }
    }

    private class BaseDemandHistory extends History<Integer> implements Observer {

        private List<Integer> values;

        public BaseDemandHistory(History<DemandDetail> demandHistory) {
            super(0);
            
            values = new ArrayList<Integer>(70);
            demandHistory.addObserver(this);
        }

        public void update(Observable o, Object arg) {
            DemandDetail detail = (DemandDetail) arg;

            int sum = 0;
            int count = 0;
            
            for (Interval interval: Interval.values()) {
                int value = detail.get(interval);
                if (value != 0) {
                    sum += value;
                    count++;
                }
            }

            if (count > 0) {
                values.add(sum / count);
                Collections.sort(values);
            }

            if (!values.isEmpty()) {
                add(values.get(values.size() / 2));
            } else {
                add(0);
            }
        }

        @Override
        public String toString() {
            return "BaseDemandHistory[last=" + last() + "]";
        }
    }
}
